#include "utility.h"

#include <iostream>
#include <iomanip>
using namespace std;

/**
 * Initialize GLUT to set up a valid openGL context.
 */
void initGLUT(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutCreateWindow("");
}


/**
 * Initialise the openGL extension entry points.
 * This also checks if the initialization has succeeded and checks if the
 * extension GLEW_ARB_fragment_program is available.
 */
void initGLEW()
{
    // Initialise GLEW, obtain function pointers for extensions.
    GLenum err = glewInit();
    if (err != GLEW_OK)
    {
        cerr << "* GLEW ERROR: " << glewGetErrorString(err) << endl;
        exit(1);
    }
    // Check if the ARB_fragment_program extension is available.
    if (!GLEW_ARB_fragment_program)
    {
        cerr << "GLEW ERROR: Required extension: GLEW_ARB_fragment_program missing." << endl;
        exit(1);
    }
}


/**
 * Set the projection matrices such that we get 1:1 mapping between pixels,
 * texels and data when rendering.
 *
 * Note:
 * A texels or texture element is the fundamental unit of texture space.
 * Textures are represented by arrays of texels just as pictures are
 * represented by arrays of pixels.
 * When a texel is requested that is outside the texture, one of the two
 * techniques is used: clamping or wrapping.
 */
void set1to1Mapping(const int size)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0.0, size, 0.0, size);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glViewport(0, 0, size, size);
}


void setupTexture(GLuint * tex, const int size, const GLenum attach,
        const float4 * h_values)
{
    // Generate the texture.
    glGenTextures(1, tex);
    checkGLErrors("setupTexture() [1]");

    const GLenum target = GL_TEXTURE_RECTANGLE_ARB;
    const GLenum internalFormat = GL_FLOAT_RGBA32_NV;
    const GLenum format = GL_RGBA;
    // GL_LUMINANCE should be used for textures with only 1 value per texel.
    const GLenum type = GL_FLOAT;

    glBindTexture(target, *tex);
    checkGLErrors("setupTexture() [2]");

    // Set texture parameters (get correct clamping)
    glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP);
    checkGLErrors("setupTexture() [3]");

    // Set texenv to replace instead of the default modulate
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    checkGLErrors("setupTexture() [4]");

    glTexImage2D(target, 0, internalFormat, size, size, 0, format, type,
            (float*)h_values);
    checkGLErrors("setupTexture() [5]");

    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attach, target, *tex, 0);
    checkFramebufferStatus("setupTexture() [6]");
}


void copyTextureFromGPU(const GLenum attachment, const int size, float4 * h_data)
{
    // NOTE have to have the correct FBO bound with glBindFrameBufferEXT()
    glReadBuffer(attachment);
    glReadPixels(0, 0, size, size, GL_RGBA, GL_FLOAT, (float*)h_data);
}


void initCg(const char * source, const char * entry,
        CGprofile & profile, CGcontext & context, CGprogram & program)
{
    context = cgCreateContext();
    if (!context)
    {
        cerr << "* CG ERROR: CG create context failed." << endl;
        exit(1);
    }

    profile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
    if (profile == CG_PROFILE_UNKNOWN)
    {
        cerr << "* CG ERROR: Invalid Fragment Program profile." << endl;
        exit(1);
    }
    cgGLSetOptimalOptions(profile);

    // create fragment program
    program = cgCreateProgramFromFile
    (
            context,           // The context where we want the shader added.
            CG_SOURCE,         // array of bytes containing the CG source.
            source,            // The source code file (char *).
            profile,           // The profile.
            entry,             // The entry function of the source code.
            0                  // Compiler options.
    );

    if (!program)
    {
        cerr << cgGetErrorString(cgGetError()) << endl;
        exit(1);
    }

    // Load program.
    cgGLLoadProgram(program);
}


void glQuad(const int size)
{
    glPolygonMode(GL_FRONT, GL_FILL);
    glBegin(GL_QUADS);
    {
        glTexCoord2f(0.0, 0.0);
        glVertex2f(0.0, 0.0);
        glTexCoord2f(size, 0.0);
        glVertex2f(size, 0.0);
        glTexCoord2f(size, size);
        glVertex2f(size, size);
        glTexCoord2f(0.0, size);
        glVertex2f(0.0, size);
    }
    glEnd();
}



void printData(const int size, const float4 * data, const std::string& msg)
{
    cout << endl;
    cout << "-----------------------------------------------" << endl;
    cout << msg << endl;
    cout << "-----------------------------------------------" << endl;
    cout << "R:" << endl;
    cout.precision(2);
    const int w = 6;
    for (int j = 0; j < size; j++) {
        for (int i = 0; i < size; i++) {
            cout << fixed << setw(w) << left << data[size * j + i].r;
        }
        cout << endl;
    }
    cout << endl;

    cout << "G:" << endl;
    for (int j = 0; j < size; j++) {
        for (int i = 0; i < size; i++) {
            cout << fixed << setw(w) << left << data[size * j + i].g;
        }
        cout << endl;
    }
    cout << endl;

    cout << "B:" << endl;
    for (int j = 0; j < size; j++) {
        for (int i = 0; i < size; i++) {
            cout << fixed << setw(w) << left << data[size * j + i].b;
        }
        cout << endl;
    }
    cout << endl;

    cout << "A:" << endl;
    for (int j = 0; j < size; j++) {
        for (int i = 0; i < size; i++) {
            cout << fixed << setw(w) << left << data[size * j + i].a;
        }
        cout << endl;
    }
    cout << "-----------------------------------------------" << endl;
    cout << endl;
}


/**
 * Checks frame buffer status.
 */
void checkFramebufferStatus(const std::string& msg)
{
    GLenum status = (GLenum) glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

    // If no error return.
    if (status == GL_FRAMEBUFFER_COMPLETE_EXT) return;

    // Otherwise: print some useful error message.

    std::string desc;
    switch(status)
    {
        case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
        {
            desc = "FB incomplete, incomplete attachment.";
            break;
        }
        case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
        {
            desc = "Unsupported FB format.";;
            break;
        }
        case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
        {
            desc = "FB incomplete, missing attachment.";
            break;
        }
        case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
        {
            desc = "FB incomplete, attached images must have same dimensions.";
            break;
        }
        case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
        {
            desc = "FB incomplete, attached images must have same format.";
            break;
        }
        case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
        {
            desc = "FB incomplete, missing draw buffer.";
            break;
        }
        case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
        {
            desc = "FB incomplete, missing read buffer.";
            break;
        }
    }
    cerr << "* FB ERROR: " << msg << endl;
    cerr << "* FB ERROR: " << desc << endl;
    exit(status);
}


void cgErrorCallback(const CGcontext& context)
{
    CGerror err = cgGetError();
    if (err)
    {
        cerr << "* CG ERROR: ";
        cerr << cgGetErrorString(err) << endl;
        cerr << cgGetLastListing(context) << endl;
        exit(err);
    }
}


void checkGLErrors(const std::string& msg)
{
    GLenum err = glGetError();
    if (err != GL_NO_ERROR)
    {
        cerr << "* OpenGL ERROR: " << msg << endl;
        cerr << "* OpenGL ERROR: " << (char*)gluErrorString(err) << endl;
        exit(err);
    }
}
